
package com.studiofortress.sf.structure;

import com.studiofortress.sf.graphics.GraphicsGL;
import com.studiofortress.sf.graphics.display.ControlClickEvent;
import com.studiofortress.sf.graphics.display.ControlEvent;
import com.studiofortress.sf.graphics.display.ControlKeyCharEvent;
import com.studiofortress.sf.graphics.display.ControlKeyEvent;
import com.studiofortress.sf.graphics.display.ControlMotionEvent;
import com.studiofortress.sf.structure.intersection.BoundsIntersection;
import com.studiofortress.sf.structure.intersection.Intersection;
import java.util.Collection;

/**
 * The basic properties that all things within a World must contain.
 * This includes location, width, a world and if it should be painted or not.
 * By default an Actor is set to be painted.
 * 
 * The width and height are by default set to 1, by they should be updated by
 * the user to reflect any changes in size (for example setting an image).
 * A lack of updating width and height can result in inaccurate collision
 * detection.
 * 
 * @author Joseph Lenton
 * @param <G> The type of graphics object this actor draws too.
 */
public class Actor<G extends GraphicsGL>
{
    private float x;
    private float y;
    
    private int width;
    private int height;
    
    private World world;
    
    // flags if this Actor should or should not be painted
    private boolean act;
    private boolean paint;
    
    // a record of the layer this actor should act and paint in
    private int actOrder;
    private int paintOrder;
    
    // flags if this Actor is waiting to be removed from the World
    private boolean awaitingRemoval;
    
    private Intersection intersection;
    
    /**
     * Creates an Actor which by default is placed at 0, 0.
     */
    public Actor()
    {
        this(0, 0);
    }
    
    public Actor(float x, float y)
    {
        this(x, y, 1, 1);
    }
    
    /**
     * Creates the actor which is placed at the given location.
     * @param x The x location of this Actor for when it is inside the World.
     * @param y The y location of this Actor for when it is inside the World.
     */
    public Actor(float x, float y, int width, int height)
    {
        this.x = x;
        this.y = y;
        
        if (width <= 0) {
            throw new IllegalArgumentException("Width cannot be less or equal to 0, width: " + width);
        }
        this.width = width;
        if (height <= 0) {
            throw new IllegalArgumentException("Height cannot be less or equal to 0, height: " + height);
        }
        this.height = height;
        
        this.world = null;

        this.act = true;
        this.paint = true;
        
        this.actOrder = 0;
        this.paintOrder = 0;
        
        intersection = new BoundsIntersection(this);
    }
    
    /**
     * This is called straight after an Actor has been added a World object.
     * By default it does nothing but override it to add specific functionality
     * to occur at that moment.
     * getWorld will return the World object it has just been added too.
     */
    protected void addedToWorld() { }
    
    /**
     * This is called directly before an Actor is to be removed from it's World
     * object, which is directly after all other Actors have acted.
     * By default this does nothing but by overriding it you can add specific
     * functionality to occur at that moment.
     * getWorld will return the World object it is about to be removed from.
     */
    protected void removedFromWorld() { }
    
    /**
     * Trivially just sets what this Actor believes is it's act layer.
     * It is presumed that the World this Actor is within is handling layers
     * correctly and is also the one calling this method.
     * 
     * This is not meant to be called manually!
     * @param actLayer The layer number for setting the order that this Actor is called in.
     */
    final void setActOrder(int actOrder)
    {
        this.actOrder = actOrder;
    }
    
    /**
     * Returns the order number used to state in when this Actor will act.
     * Lower order numbers act first, higher order numbers act last.
     * @return The layer number that this Actor has defined for having it's Act method called from.
     */
    public final int getActOrder()
    {
        return actOrder;
    }
    
    /**
     * Trivially just sets what this Actor believes is it's painting layer.
     * It is presumed that the World this Actor is within is handling layers
     * correctly and is also the one calling this method.
     * 
     * This is not meant to be called manually!
     * @param paintLayer The layer number that this actor is painted too.
     */
    final void setPaintOrder(int paintOrder)
    {
        this.paintOrder = paintOrder;
    }
    
    /**
     * The order number for when this actor will paint.
     * @return The painting layer that this Actor is painted too.
     */
    public final int getPaintOrder()
    {
        return paintOrder;
    }
    
    /**
     * If this Actor is currently inside of a World then it is removed form
     * that World. It is then added to the given World, if it isn't null.
     * If it is null then this Actor has no World.
     * @param world The World to place this Actor in or null if it is to be removed from a World.
     */
    void setWorld(World world)
    {
        this.world = world;
    }
    
    /**
     * For retrieving the world that this Actor is within.
     * @return The current World this Actor is within, or null if it is not within one.
     */
    public World getWorld()
    {
        return world;
    }
    
    /**
     * Scans up the chain of parent Worlds above this Actor, looking for the
     * first World which implements the given class. Once found it will be
     * returned.
     * 
     * Becareful, because this includes all parent worlds not just the direct
     * parent of this one.
     * @param <B> The class or intterface of the World to return.
     * @param worldClass The Class of the World to find, cannot be null.
     * @return Null if no world was found, otherwise the first parent of this Actor which implements the given class.
     */
    public <B> B getWorld(Class<B> worldClass)
    {
        if (worldClass == null) {
            throw new IllegalArgumentException("The given world type cannot be null.");
        }

        for (World parent = getWorld(); parent != null; parent = parent.getWorld()) {
            if (worldClass.isInstance(parent)) {
                return (B) parent;
            }
        }

        return null;
    }

    /**
     * True or false in order to state if this Actor is currently assigned into
     * a world or not. It is in a World if getWorld() returns a non-null value.
     * @return True if this Actor is within a World, otherwise false.
     */
    public boolean isInWorld()
    {
        return getWorld() != null;
    }
    
    /**
     * Informs that the World it is in should update it's knowledge about this
     * Actor. If this is not in a World then nothing will happen.
     * This is typically called when size or location has changed.
     */
    void update()
    {
        if (isInWorld()) {
            getWorld().updateActor( this );
        }
    }
    
    /**
     * Removes this Actor from the world it is currently in. If the Actor is
     * not in a World then nothing will happend.
     * The equivalent of calling 'getWorld().removeActor(this)'
     */
    public void remove()
    {
        if (isInWorld()) {
            getWorld().removeActor( this );
        }
    }

    /**
     * States if this Actor is expecting or not expecting to be removed soon.
     * It is mainly so that the World can track which Actor are to be removed
     * and which aren't.
     * @param awaitingRemoval True if this Actor is expecting to be removed soon, otherwise false.
     */
    void setAwaitingRemoval(boolean awaitingRemoval)
    {
        this.awaitingRemoval = awaitingRemoval;
    }
    
    /**
     * @return True if this Actor is waiting to be removed from the world, otherwise false.
     */
    boolean isAwaitingRemoval()
    {
        return awaitingRemoval;
    }
    
    /**
     * Sets the Actor at the new given location.
     * Same as called setX and setY together.
     * @param x The new location along the x-axis.
     * @param y The new location along the y-axis.
     */
    public void setLocation(float x, float y)
    {
        setX( x );
        setY( y );
    }
    
    /**
     * Changes the x location of this Actor.
     * @param x The new location of this Actor on the X-Axis.
     */
    public void setX(float x)
    {
        this.x = x;
        
        update();
    }

    /**
     * Changes the Y location of this Actor.
     * @param y The new location of this Actor on the Y-Axis.
     */
    public void setY(float y)
    {
        this.y = y;
        
        update();
    }

    /**
     * Retrieves the X location of this Actor.
     * @return The current X position of this Actor.
     */
    public float getX()
    {
        return x;
    }

    /**
     * Retrieves the Y location of this Actor.
     * @return The current Y position of this Actor.
     */
    public float getY()
    {
        return y;
    }
    
    /**
     * Moves the Actor along the x and y axis by the given amounts.
     * @param moveX The amount to move along the x axis.
     * @param moveY The amount to move along the y axis.
     */
    public void move(float moveX, float moveY)
    {
        setLocation( getX() + moveX, getY() + moveY );
    }
    
    /**
     * Sets the new width and height of this actor. It is the equivalent of
     * calling setWidth and then setHeight consecutively with the given values.
     * Width and height cannot be less or equal to 0.
     * @param width What is considered to be the true width of the Actor.
     * @param height What is considered to be the true height of the Actor.
     */
    public void setSize(int width, int height)
    {
        setWidth(  width  );
        setHeight( height );
    }

    /**
     * Sets the width of the actor to a new value, the width must be greater
     * then 0.
     * @param width What is considered to be the true width of the Actor.
     */
    public void setWidth(int width)
    {
        if (width <= 0) {
            throw new IllegalArgumentException("Width cannot be less or equal to 0, width: " + width);
        }
        
        this.width = width;
        
        update();
    }
    
    /**
     * Sets the height of the actor, which must be greater then 0.
     * @param height What is considered to be the true height of the Actor.
     */
    public void setHeight(int height)
    {
        if (height <= 0) {
            throw new IllegalArgumentException("Height cannot be less or equal to 0, height: " + height);
        }

        this.height = height;
        update();
    }
    
    /**
     * Retrieves the width of the actor.
     * @return The width of the Actor.
     */
    public int getWidth()
    {
        return width;
    }
    
    /**
     * Retrieves the height of the Actor.
     * @return The height of the Actor.
     */
    public int getHeight()
    {
        return height;
    }

    /**
     * A flag to state if this Actor should or should not have it's act method
     * called. It is simply a hint to the World class.
     * By default this is set to true.
     * This has no effect on if this Actor should or shouldn't be painted.
     * @param act True if this Actor should act, otherwise false.
     */
    public void setAct(boolean act)
    {
        this.act = act;
    }

    /**
     * For checking if this Actor is set to act or not.
     * @return True if this Actor should act, otherwise false.
     */
    public boolean isAct()
    {
        return act;
    }
    
    /**
     * If set to true then this Actor will be painted on each frame.
     * If set to false then it will be.
     * This has no effect on if this Actor should or should not act.
     * @param paint If true then this Actor will be painted, if false then it won't be.
     */
    public void setPaint(boolean paint)
    {
        this.paint = paint;
    }
    
    /**
     * For checking if this Actor should paint or not.
     * @return True if this Actor is to be painted, otherwise false.
     */
    public boolean isPaintEnabled()
    {
        return paint;
    }
    
    /**
     * The main Act method of this Actor. This is called and performed on every
     * frame for this Actor when it is inside of a World.
     * 
     * This is designed to be overriden by the user as by default it does
     * nothing.
     */
    public void act() { }
    
    /**
     * The real paint call that should be made by the World. It adds painting
     * options on top of the standard Actor painting.
     * Calls this Actor to paint if it is not hidden.
     * @param g 
     */
    void realPaint(G g)
    {
        if (isPaintEnabled()) {
            paint(g);
        }
    }
    
    /**
     * This is the standard paint method of this Actor. It will be called after
     * all Actors have acted within a frame. It is only called if this Actor
     * is not hidden.
     * 
     * This is designed to be overriden by the user as by default it does
     * nothing.
     */
    public void paint(G g) { }

    /**
     * Sets the intersection object to use when this Actor is checked in an
     * intersection against other Actors.
     * @param intersection The new intersection this Actor will use for collision detection, cannot be null.
     */
    public void setIntersection(Intersection intersection)
    {
        if (intersection == null) {
            throw new IllegalArgumentException("The new Intersection cannot be null.");
        }
        this.intersection = intersection;
    }
    
    /**
     * Retrieves the Intersection object representing this Actor during an
     * intersection check.
     * @return The intersection used by this Actor during collision detection.
     */
    public Intersection getIntersection()
    {
        return intersection;
    }
    
    /**
     * Is this Actor is within a World then this will return a list that
     * contains all the Actors of the given classes that intersect this Actor.
     * If no Actors collide then an empty list is returned.
     * @param collisionClasses The classes of Actor to check for a collision.
     * @return A list containing all the Actors that collide with this Actor of the given classes.
     * @throws IllegalStateException If this is called whilst this Actor is not within a World.
     */
    public Collection<Actor> getIntersectingActors(Class<? extends Actor> ... collisionClasses)
    {
        if (isInWorld()) {
            return getWorld().getIntersectingActors( this, collisionClasses );
        } else {
            throw new IllegalStateException("This should not be called if the Actor is not in the World.");
        }
    }
    
    /**
     * Is this Actor is within a World then this will return an Actor of one of
     * the given classes that intersect this Actor. There is no guarantee that
     * the same Actor will always be returned under the same conditions.
     * If no Actors collide then null is returned.
     * @param collisionClasses The classes of Actor to check for a collision.
     * @return An Actor that collide with this Actor of the given classes, or null if no Actor is found.
     * @throws IllegalStateException If this is called whilst this Actor is not within a World.
     */
    public Actor getIntersectingActor(Class<? extends Actor> ... collisionClasses)
    {
        if (isInWorld()) {
            return getWorld().getIntersectingActor( this, collisionClasses );
        } else {
            return null;
        }
    }

    /**
     * Helper function to ask the World to apply the given action to all the
     * Actors of the given classes that intersect this Actor.
     * @param <A> The type of Actor that this Actor's World contains.
     * @param action The action to perform on all intersecting actors, cannot be null.
     * @param collisionClasses The types of Actors to check against.
     * @return True if one or more Actors intersected this one.
     */
    public <A extends Actor> boolean applyToIntersectingActors(Action<A> action, Class<? extends Actor> ... collisionClasses)
    {
        return getWorld().applyToIntersectingActors( (A) this, action, collisionClasses );
    }

    /**
     * Helper function to ask this Actors World to apply the given action to an
     * Actor of one of the given classes, which also intersects this Actor.
     *
     * This will only apply the action to one Actor, not multiple Actors, that
     * intersects this Actor.
     * @param <A> The type of Actors stored in this Actors parent World.
     * @param action The action to perform on the first Actor this intersects, cannot be null.
     * @param collisionClasses The types of intersecting Actors to search for, cannot be null.
     * @return True if an Actor did intersect this Actor, otherwise false.
     */
    public <A extends Actor> boolean applyToIntersectingActor(Action<A> action, Class<? extends Actor> ... collisionClasses)
    {
        return getWorld().applyToIntersectingActor( this, action, collisionClasses );
    }
    
    /**
     * Determines if this Actor intersects the given Actor.
     * @param other The Actor to check an intersection against.
     * @return True if this Actor intersects the other Actor.
     */
    public boolean intersects(Actor other)
    {
        return getIntersection().isIntersection( other.getIntersection() );
    }

    /**
     *
     * @param event
     */
    void handleEvent(ControlEvent event)
    {
        if ( event instanceof ControlClickEvent ) {
            handleClickEvent( (ControlClickEvent) event );
        } else if ( event instanceof ControlKeyEvent ) {
            handleKeyEvent( (ControlKeyEvent) event );
        } else if ( event instanceof ControlKeyCharEvent ) {
            handleKeyCharEvent( (ControlKeyCharEvent) event );
        } else if ( event instanceof ControlClickEvent ) {
            handleMotionEvent( (ControlMotionEvent) event );
        }
    }

    /**
     * Is run when this receives a ControlClickEvents. Override it to add your
     * own event handling code.
     *
     * By default this does nothing.
     *
     * WARNING! You need to activate events in your display before you can use
     * this.
     *
     * @param event The click event to handle.
     */
    protected void handleClickEvent( ControlClickEvent event )
    {
        // do nothing
    }

    /**
     * Is run when this receives a ControlKeyEvents. Override it to add your own
     * event handling code. This is for handling key presses and releases.
     *
     * By default this does nothing.
     *
     * WARNING! You need to activate events in your display before you can use
     * this.
     *
     * @param event The key event to handle.
     */
    protected void handleKeyEvent( ControlKeyEvent event )
    {
        // do nothing
    }

    /**
     * Is run when this receives a ControlKeyCharEvent. This differes to a
     * ControlKeyEvent in that it deals with a character being types into the
     * GraphicsDisplay.
     *
     * By default this does nothing.
     *
     * WARNING! You need to activate events in your display before you can use
     * this.
     *
     * @param event The key event to handle.
     */
    protected void handleKeyCharEvent( ControlKeyCharEvent event )
    {
        // do nothing
    }

    /**
     * Is run when this receives a ControlMotionEvents. Override it to add your
     * own event handling code.
     *
     * By default this does nothing.
     *
     * WARNING! You need to activate events in your display before you can use
     * this.
     * 
     * @param event The motion event to handle.
     */
    protected void handleMotionEvent( ControlMotionEvent event )
    {
        // do nothing
    }
}
